import json
import logging
import os
import re

from PyQt5 import QtNetwork, QtGui
from PyQt5 import QtCore
from PyQt5.QtCore import QFile, QIODevice, QByteArray, Qt, qAbs, QRect, QSize, pyqtSignal
from PyQt5.QtGui import QPixmap, QPainter, QGuiApplication, QColor, QPen, QIcon
from PyQt5.QtWidgets import QMessageBox, QWidget, QApplication, QPushButton, QDesktopWidget
from datetime import datetime


class FznChatSender:
    """
    使用同一个network，但是针对每一次发送消息都声明一个FznChatSender实例。

    Attributes:
        network: 进行通讯的的网络对象
        log: 日志对象
        reply: 每一次和服务器通讯的响应
        finished_handler: 客户端收到服务器响应后的回调函数
        out_file: 下载文件时的输出文件对象
        in_file: 上传的文件
        send_message_handler: 文件长传完成后执行的回调函数
        fullname: 文件的全路径
        file_size: 文件大小，单位为字节
        user_canceled: 用户取消上传文件
        download_error_handler: 发生错误时的回调函数
        error_code: 错误代码
        download_finished_handler: 下载完成后的回调函数
        download_progress_handler: 下载进度回调函数
    """

    def __init__(self, network, log=None):
        self.network = network
        self.log = log
        self.reply = None
        self.finished_handler = None
        self.out_file = None
        self.in_file = None
        self.send_message_handler = None
        self.fullname = None
        self.file_size = 0
        self.user_canceled = False
        self.download_error_handler = None
        self.error_code = None
        self.download_finished_handler = None
        self.download_progress_handler = None

    def send_msg(self, request: QtNetwork.QNetworkRequest, finished_handler=None) -> None:
        """

        :param request:之前构造好的请求，包括请求的地址、Header
        :param finished_handler:请求完成之后的处理函数，因为使用了QNetworkReply的信号，所以需要使用匿名函数再包装一次
        :return:
        """
        self.finished_handler = finished_handler

        data = QtCore.QByteArray()
        if self.log:
            self.log.logger.info("发送post消息")
        self.reply = self.network.manager.post(request, data)
        self.reply.finished.connect(self.reply_finished_handler)

        self.network.add_sender(self)

    def reply_finished_handler(self) -> None:
        """
        服务器请求回来以后的回调函数，通过这个回调函数执行用户传入的回调函数，同时代入服务器响应。
        :return:
        """
        if self.log:
            self.log.logger.info("收到服务器反馈消息")

        if self.finished_handler:
            self.finished_handler(self.reply)

        self.network.remove_sender(self)

    def close_file(self) -> None:
        """
        文件下载完成以后要关闭文件。
        :return:
        """
        if self.log:
            self.log.logger.info("开始关闭文件")

        self.out_file.flush()
        if self.error_code is not None:
            # 在发生错误的时候，删除已经下载的文件
            self.out_file.remove()
        else:
            self.out_file.close()

        # 执行传入的回调函数
        if self.download_finished_handler is not None:
            self.download_finished_handler()

        self.network.remove_sender(self)

    def write_file(self) -> None:
        """
        文件有响应的话，通过这个函数写入文件内容。
        :return:
        """
        if self.out_file:
            self.out_file.write(self.reply.readAll())

    def download_file(self, request: QtNetwork.QNetworkRequest, path: str, download_error_handler=None,
                      download_finished_handler=None, download_progress_handler=None) -> bool:
        """
        从服务器下载指定的文件
        :param request:
        :param path:本地文件的路径
        :param download_error_handler:发生错误时的回调函数
        :param download_finished_handler: 文件下载完成后的回调
        :param download_progress_handler: 文件进度显示回调
        :return:
        """
        if self.log:
            self.log.logger.info("开始下载文件:{}".format(path))

        # 先把回调函数保存下来
        self.download_error_handler = download_error_handler
        self.download_finished_handler = download_finished_handler
        self.download_progress_handler = download_progress_handler

        # 打开文件
        self.fullname = path
        self.out_file = QFile(self.fullname)
        if not self.out_file.open(QIODevice.WriteOnly):
            return False

        data = QtCore.QByteArray()
        self.reply = self.network.manager.post(request, data)

        self.reply.finished.connect(self.close_file)
        self.reply.readyRead.connect(self.write_file)
        if self.download_error_handler:
            self.reply.error.connect(self.download_error_handler)
        # 设置进度显示的回调函数
        if self.download_progress_handler:
            self.reply.downloadProgress.connect(self.download_progress_handler)

        self.network.add_sender(self)
        return True

    def error_occurred(self, code):
        """
        执行传入的错误回调,因为参数不一样，所以又包装了一层。
        :param code: 错误代码
        :return:
        """
        # 如果是用户主动取消的，那么直接返回就好了
        if self.user_canceled:
            return

        self.error_code = code
        self.download_error_handler()

    def close_connection(self) -> None:
        """
        用户主动取消上传
        :return:
        """
        if self.reply:
            if self.log:
                self.log.logger.info("关闭文件")
            self.user_canceled = True
            # 这里没有使用 self.reply.close()是因为，close()会继续执行，直到结束。
            self.reply.abort()

    def close_infile(self) -> None:
        """
        文件上传服务器完成时的回调函数。正常结束和用户主动取消都会触发这个函数。
        :return:
        """
        # 用户主动取消上传的话，下面这些就不用执行了
        if self.user_canceled:
            return

        if self.log:
            self.log.logger.info("文件上传完成")

        # 如果用户上传文件失败，提示用户
        error = self.reply.error()
        if error != 0:
            QMessageBox.warning(None, '财讯通', '上传文件出错')
            return

        # 返回信息解析后为一个字典
        reply_data = json.loads(str(self.reply.readAll(), 'utf-8'))
        if reply_data['code'] == -1:
            if self.log:
                self.log.logger.info(reply_data)

            QMessageBox.warning(None, '财讯通', '上传文件出错')
            return

        # 新的路径名
        new_path = reply_data['msg']
        # 原来的文件名，暂时没有用到
        # ori_filename = reply_data['data']

        # 向目标转发文本消息
        if self.send_message_handler:
            self.send_message_handler(new_path, self.fullname, self.file_size)

        self.network.remove_sender(self)

    def upload_file(self, request: QtNetwork.QNetworkRequest, filename: str, dst_id: str, send_message_handler=None,
                    show_progress_handler=None):
        """上传任意文件到服务器，上传成功后将文件类型的消息发送给目标。

        :param request: 请求对象
        :param filename: 文件全称，包含文件的路径和文件名
        :param dst_id: 目标的ID，可以是群的ID，也可以是用户的ID
        :param send_message_handler: 文件上传完成后，执行这个回调函数向目标发送文件类型的消息
        :param show_progress_handler: 文件上传进度的显示
        :return:
        """
        # 必须要指定一个目标
        if dst_id is None:
            return

        # 打开要发送的文件
        self.in_file = QtCore.QFile(filename)
        if not self.in_file.open(QtCore.QIODevice.ReadOnly):
            return

        # 文件上传完成后使用这个函数发送信息到服务器
        self.send_message_handler = send_message_handler
        # 保存文件的全名和大小
        self.fullname = filename
        self.file_size = self.in_file.size()

        # 构建表单
        multi_part = QtNetwork.QHttpMultiPart(QtNetwork.QHttpMultiPart.FormDataType)
        post_part = QtNetwork.QHttpPart()
        post_part.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/binary")
        post_part.setHeader(QtNetwork.QNetworkRequest.ContentDispositionHeader,
                            'form-data;name="upload_file";filename="{}"'.format(self.in_file.fileName())
                            )
        post_part.setBodyDevice(self.in_file)
        self.in_file.setParent(multi_part)

        multi_part.append(post_part)
        multi_part.setBoundary(QByteArray("boundary_fznChat".encode()))

        request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader,
                          "multipart/form-data;boundary={}".format(multi_part.boundary().data().decode()))
        # 设置目标用户的ID
        request.setRawHeader("dstId".encode("utf-8"), dst_id.encode("utf-8"))

        # 发送请求
        self.reply = self.network.manager.post(request, multi_part)
        multi_part.setParent(self.reply)

        # 请求完成时的回调函数
        self.reply.finished.connect(self.close_infile)
        # 显示上传进度
        if show_progress_handler is not None:
            self.reply.uploadProgress.connect(show_progress_handler)

        self.network.add_sender(self)


class FznChatNetWork:
    """
    封装对网络的操作。

    Attributes:
        manager: 网络管理器，程序只使用这一个实例
        logger: 日志对象
        server: 服务器地址
        port: 服务器端口
        sender_set: 存储sender的set，以免sender被提前清理。
    """

    def __init__(self, parent, server: str, port: str, logger=None):
        """

        :param parent:用来构建QNetworkAccessManager时使用的
        :param server:服务器地址
        :param port:服务器端口
        :param logger:记录信息的日志实例
        """
        self.manager = QtNetwork.QNetworkAccessManager(parent)
        self.logger = logger
        self.server = server
        self.port = port
        self.sender_set = set()

    def new_request(self, path: str, set_header=None) -> QtNetwork.QNetworkRequest:
        """
        根据用户输入的路径返回构建好的请求，这个请求可以用在后面对服务器的实际访问上。
        :param path:路径不包含地址和端口，也不包括前面的/
        :param set_header:设置额外header的函数，如果这个函数不为None，会调用这个函数来设置
        :return:
        """
        url = "http://{}:{}/{}".format(self.server, self.port, path)
        request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
        request.setHeader(QtNetwork.QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded")
        if set_header:
            set_header(request)
        return request

    def add_sender(self, sender: FznChatSender) -> None:
        """
        保存FznChatSender类的实例，以免提前销毁导致的异步动作无法完成。
        :param sender:
        :return:
        """
        self.sender_set.add(sender)

    def remove_sender(self, sender: FznChatSender) -> None:
        """
        把已经保存的FznChatSender对象移除出来，后续系统就可以销毁了。这个动作一般是在
        :param sender:
        :return:
        """
        self.sender_set.remove(sender)


class Logger(object):
    """
    全局使用一个实例来输出日志。
    """

    def __init__(self, level='info'):
        """
        后期只要修改日志的级别，就可以不再输出特定的信息。
        :param level:
        """
        self.fmt = '%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s'
        # 日志级别关系映射
        self.level_relations = {
            'debug': logging.DEBUG,
            'info': logging.INFO,
            'warning': logging.WARNING,
            'error': logging.ERROR,
            'crit': logging.CRITICAL
        }
        self.logger = logging.getLogger()
        # 设置日志格式
        format_str = logging.Formatter(self.fmt)
        # 设置日志级别
        self.logger.setLevel(self.level_relations.get(level))
        # 往屏幕上输出
        sh = logging.StreamHandler()
        # 设置屏幕上显示的格式
        sh.setFormatter(format_str)
        # 把对象加到logger里
        self.logger.addHandler(sh)


def str_to_datetime(datetime_str: str) -> datetime:
    """
    把使用的字符串格式的日期转换为datetime格式。
    :param datetime_str:
    :return:
    """
    return datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S')


def show_warning_message(parent: QWidget = None, text: str = None, informative_text: str = None,
                         selection: bool = False, minimum: bool = False) -> int:
    """
    以中文提示用户信息
    :param parent:
    :param text:
    :param informative_text:
    :param selection: 用户是不是可以选择同意或取消
    :param minimum: 是不是可以让用户选择最小化
    :return:
    """
    msg_box = QMessageBox(parent=parent)
    msg_box.setWindowTitle('财讯通')
    msg_box.setIcon(QMessageBox.Warning)

    if text:
        # msg_box.setText(text)
        msg_box.setInformativeText(text)

    if informative_text:
        # msg_box.setInformativeText(informative_text)
        msg_box.setText(informative_text)

    ok = msg_box.addButton('确定', QMessageBox.AcceptRole)
    if selection:
        msg_box.addButton('取消', QMessageBox.RejectRole)
        if minimum:
            msg_box.addButton('最小化', QMessageBox.ActionRole)

    msg_box.setDefaultButton(ok)
    return msg_box.exec()


def get_pixmap_by_file_ext(fullname: str) -> QPixmap:
    """
    根据文件的扩展名来返回一个图标
    :param fullname:
    :return:
    """
    ext = os.path.splitext(fullname)[-1].lower()
    if ext == '.doc' or ext == '.docx':
        return QPixmap(':/file/resources/images/doc.png')
    elif ext == '.xls' or ext == '.xlsx':
        return QPixmap(':/file/resources/images/xls.png')
    elif ext == '.pdf':
        return QPixmap(':/file/resources/images/pdf.png')
    elif ext == '.ppt' or ext == '.pptx':
        return QPixmap(':/file/resources/images/ppt.png')
    elif ext == '.txt':
        return QPixmap(':/file/resources/images/txt.png')
    elif ext == '.zip' or ext == '.rar' or ext == '.7z':
        return QPixmap(':/file/resources/images/zip.png')
    elif ext == '.mp3' or ext == '.wav' or ext == '.cda':
        return QPixmap(':/file/resources/images/music-file.png')
    elif ext == '.avi' or ext == '.wmv' or ext == '.mpeg' or ext == '.mpg' or ext == '.mp4' or ext == '.mov':
        return QPixmap(':/file/resources/images/multimedia.png')
    elif ext == '.exe' or ext == '.com':
        return QPixmap(':/file/resources/images/exe.png')
    elif ext == '.jpg' or ext == '.jpeg' or ext == '.png' or ext == '.gif':
        return QPixmap(':/file/resources/images/picture.png')
    else:
        return QPixmap(':/file/resources/images/download.png')


def is_pic_file(fullname: str) -> bool:
    """
    判断文件是否为图片
    :param fullname:
    :return:
    """
    ext = os.path.splitext(fullname)[-1].lower()
    if ext == '.jpg' or ext == '.jpeg' or ext == '.png' or ext == '.gif':
        return True
    else:
        return False


# 解析每一段文字使用的正则表达式
re_paragraph = re.compile("<p.*?>(.*?)</p>", re.MULTILINE)
re_span = re.compile("<span.*?>(.*?)</span>")
# 替换空行的正则表达式
re_newline = re.compile("<br />")
# 提取表情地址的正则表达式
re_emoji = re.compile("<img src=.*?:/emoji/resources/emoji/(.*?)\".*? />")


def html_to_plaintext(html_msg: str) -> str:
    """
    取出HTML格式的用户输入消息，取出每一段中的内容组合成一个新的消息，需要对换行和图象进行特殊处理.
    :param html_msg: 待转换的HTML消息
    :return:
    """
    # 取出所有<p></p>之间的内容，这里使用的是非贪婪模式
    paragraphs = re_paragraph.findall(html_msg)
    plaintext = []
    for line in paragraphs:
        spans = re_span.findall(line)
        if len(spans) > 0:
            line = "".join(spans)
        # 把<br />替换成回车
        line = re_newline.sub("", line)
        # 把图象替换成表情，表情前后有两个冒号来标识
        line = re_emoji.sub(r":\1:", line)
        plaintext.append(line)
    return '\n'.join(plaintext)


class CaptureScreen(QWidget):
    # 初始化变量
    beginPosition = None
    endPosition = None
    fullScreenImage = None
    captureImage = None
    isMousePressLeft = None
    painter = QPainter()
    signalSendFile = pyqtSignal(str)

    def __init__(self):
        super(QWidget, self).__init__()

        # 初始化确定及取消按钮
        self.btn_ok = QPushButton(self)
        self.btn_cancel = QPushButton(self)

    def setup_ui(self, send_image_handler):
        """
        初始化设置用到的控件
        :return:
        """
        # 初始化窗口
        self.init_window()
        self.init_buttons()
        self.signalSendFile.connect(send_image_handler)

    def init_buttons(self):
        """
        初始化使用的按钮，现在是只有保存和取消两个。
        :return:
        """
        self.btn_ok.resize(36, 36)
        self.btn_ok.setIcon(QIcon(":/main/resources/images/accept.png"))
        self.btn_ok.setIconSize(QSize(36, 36))
        self.btn_ok.setFlat(True)
        self.btn_ok.clicked.connect(self.button_ok_clicked)
        self.btn_ok.hide()

        self.btn_cancel.resize(36, 36)
        self.btn_cancel.setIcon(QIcon(":/main/resources/images/cancel.png"))
        self.btn_cancel.setIconSize(QSize(36, 36))
        self.btn_cancel.setFlat(True)
        self.btn_cancel.clicked.connect(self.button_cancel_clicked)
        self.btn_cancel.hide()

    def button_cancel_clicked(self):
        """
        取消按钮点击之后之后简单的关闭窗体.
        :return:
        """
        self.close()

    def button_ok_clicked(self):
        """
        点击确定按钮
        :return:
        """
        if self.captureImage is not None:
            filename = self.save_image()
            self.close()
            self.signalSendFile.emit(filename)

    def init_window(self):
        """
        初始化窗体自身的设置。
        :return:
        """
        # 鼠标追踪
        self.setMouseTracking(True)
        # 窗口无边框
        # self.setWindowFlag(Qt.FramelessWindowHint)
        self.setWindowFlags(Qt.FramelessWindowHint)
        # 窗口全屏
        self.setWindowState(Qt.WindowFullScreen)
        self.setWindowTitle("截屏")

    def capture_full_screen(self):
        """
        真正的截屏操作。
        :return:
        """
        self.fullScreenImage = QGuiApplication.primaryScreen().grabWindow(QApplication.desktop().winId())

    def hide_buttons(self):
        """
        隐藏按钮
        :return:
        """
        self.btn_ok.hide()
        self.btn_cancel.hide()

    def mousePressEvent(self, event):
        """
        鼠标左键或右键按下的时候，左键比较简单，但是右键操作就有区分了。
        :param event:
        :return:
        """
        # 设置光标形状为十字
        self.setCursor(Qt.CrossCursor)

        if event.button() == Qt.LeftButton:
            # 左键按下的时候，记录起始的位置
            self.beginPosition = event.pos()
            self.isMousePressLeft = True
        elif event.button() == Qt.RightButton:
            # 先把按钮都隐藏起来
            self.hide_buttons()
            # 如果选取了图片,则按一次右键开始重新截图
            if self.captureImage is not None:
                self.captureImage = None
                self.paint_background_image()
                self.update()
            else:
                # 没有选取图片的话，单击右键是退出
                self.close()

    def mouseMoveEvent(self, event):
        """
        鼠标移动的时候，如果按钮已经显示出来了，先隐藏。记录鼠标的位置，而且要重绘窗体。
        :param event:
        :return:
        """
        if self.isMousePressLeft is True:
            self.hide_buttons()
            self.endPosition = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):
        # 恢复光标形状为箭头
        self.setCursor(Qt.ArrowCursor)
        # 记录鼠标结束的位置
        self.endPosition = event.pos()
        self.isMousePressLeft = False

        # 如果按钮已经显示了，那么就不再继续了；如果保存或者取消按钮没有显示，才继续计算显示位置。
        if not self.btn_ok.isHidden() or not self.btn_cancel.isHidden():
            return
        else:
            # 释放的是鼠标左键,并且现在已经有了截图的话,才显示两个按钮
            if event.button() == Qt.LeftButton and self.captureImage is not None:
                resolution = QDesktopWidget().screenGeometry(-1)
                x = self.endPosition.x()
                y = self.endPosition.y()
                btn_width = self.btn_ok.width()
                # 左右调整鼠标的显示位置
                if x + btn_width >= resolution.width():
                    x -= btn_width
                if x - btn_width < 0:
                    x += btn_width
                if y + self.btn_ok.height() >= resolution.height():
                    y -= self.btn_ok.height()
                self.btn_ok.move(x, y)
                self.btn_ok.show()

                x -= btn_width
                self.btn_cancel.move(x, y)
                self.btn_cancel.show()
            else:
                self.hide_buttons()

    def mouseDoubleClickEvent(self, event):
        """
        双击鼠标左键保存文件
        :param event:
        :return:
        """
        if self.captureImage is not None:
            self.save_image()
            self.close()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Escape:
            # ESC关闭
            self.close()
        elif event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return:
            # 回车保存文件
            if self.captureImage is not None:
                self.save_image()
                self.close()

    def paint_background_image(self):
        """
        用黑色办透明绘制背景图
        :return:
        """
        shadow_color = QColor(0, 0, 0, 100)  # 黑色半透明
        self.painter.drawPixmap(0, 0, self.fullScreenImage)
        self.painter.fillRect(self.fullScreenImage.rect(), shadow_color)  # 填充矩形阴影

    def paintEvent(self, event):
        # 开始重绘
        self.painter.begin(self)
        self.paint_background_image()

        pen_color = QColor(30, 144, 245)  # 画笔颜色
        self.painter.setPen(QPen(pen_color, 1, Qt.SolidLine, Qt.RoundCap))  # 设置画笔,蓝色,1px大小,实线,圆形笔帽
        if self.isMousePressLeft is True:
            pick_rect = self.get_rectangle(self.beginPosition, self.endPosition)  # 获得要截图的矩形框
            self.captureImage = self.fullScreenImage.copy(pick_rect)  # 捕获截图矩形框内的图片
            self.painter.drawPixmap(pick_rect.topLeft(), self.captureImage)  # 填充截图的图片
            self.painter.drawRect(pick_rect)  # 画矩形边框
        self.painter.end()  # 结束重绘

    @staticmethod
    def get_rectangle(begin_point, end_point):
        """
        计算要截取的矩形框
        :param begin_point:
        :param end_point:
        :return:
        """
        pick_rect_width = int(qAbs(begin_point.x() - end_point.x()))
        pick_rect_height = int(qAbs(begin_point.y() - end_point.y()))
        pick_rect_top = begin_point.x() if begin_point.x() < end_point.x() else end_point.x()
        pick_rect_left = begin_point.y() if begin_point.y() < end_point.y() else end_point.y()
        pick_rect = QRect(pick_rect_top, pick_rect_left, pick_rect_width, pick_rect_height)
        # 避免高度宽度为0时候报错
        if pick_rect_width == 0:
            pick_rect.setWidth(2)
        if pick_rect_height == 0:
            pick_rect.setHeight(2)

        return pick_rect

    def save_image(self) -> str:
        # 获得起始执行目录
        filename = os.path.join(os.path.split(os.path.realpath(__file__))[0], "tmp",
                                '{}.png'.format(datetime.now().strftime("%Y%m%d%H%M%S")))
        # filename = os.path.join(os.path.split(os.path.realpath(__file__))[0], "tmp", f'{datetime.now().strftime("%Y%m%d%H%M%S")}.png')
        self.captureImage.save(filename, quality=95)  # 保存图片到当前文件夹中
        return filename

    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
        self.hide_buttons()
        self.captureImage = None
        self.beginPosition = None
        self.isMousePressLeft = None
        event.accept()
